﻿// Copyright (c) Microsoft Corporation.  All rights reserved. 
using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace WiE.Model
{
    /// <summary>
    /// This class implements a simple synchronization server to keep data from the local data model in synch
    /// with the remote data model.  This class and infrastructure should be replaceable by the new ADO.Net Sync services and 
    /// I plan to switch to that mechanism once I have this implementation working (to learn and compare the two approaches).
    /// </summary>
    class WiEModelSynch
    {
        private static long DELAY_IN_MINUTES_BEFORE_FIRST_CHECK = 1;        // Wait a minute before making first check against server
        private static long DELAY_IN_MINUTES_BEFORE_NEXT_CHECK = 1;         // Attempt to minimize connections, only attempt every 5 minutes
        private static long DELAY_IN_MINUTES_MINIMUM_BETWEEN_CHECK = 1;

        /// <summary>
        /// Responsible for background processing of the replication events between the two data models.
        /// </summary>
        private Thread m_threadSynchWorker = null;
        private volatile bool m_bKeepRunning = true;
        private AutoResetEvent m_threadResetEvent = null;
        private DateTime m_dtNextCheck;

        // Optimization to minimize synchronization of infrequently changing data
        private volatile bool m_bSyncMembers = true;

        /// <summary>
        /// We may be able to replace this with a generic approach that handles "n" models, and does something akin
        /// to peer to peer replication among the "n" sources.
        /// </summary>
        private IWiEModel m_localModel;
        private IWiEModel m_remoteModel;

        /// <summary>
        /// The Synch object is responsible for keeping two models aware of changes and synchronize the data between
        /// the two models (when appropriate).
        /// </summary>
        public IWiEModel LocalModel
        {
            get
            {
                return m_localModel;
            }
            set
            {
                m_localModel = value;

                if (m_localModel != null)
                {
                    // We need to listen to the events raised from the model
                    m_localModel.RaiseLocationHistoryChangedEvent += new EventHandler<LocationHistoryChangedArgs>(OnLocationHistoryChangedEvent);
                    m_localModel.RaiseDeviceChangedEvent += new EventHandler<DeviceChangedArgs>(OnDeviceChangedEvent);
                    m_localModel.RaiseMemberChangedEvent += new EventHandler<MemberChangedArgs>(OnMemberChangedEvent);
                }
            }
        }

        /// <summary>
        /// Specify the remote model that is to be kept in synch with the local model.
        /// </summary>
        public IWiEModel RemoteModel
        {
            get
            {
                return m_remoteModel;
            }
            set
            {
                m_remoteModel = value;

                if (m_remoteModel != null)
                {
                    // We need to listen to events from the model
                    m_remoteModel.RaiseLocationHistoryChangedEvent += new EventHandler<LocationHistoryChangedArgs>(OnLocationHistoryChangedEvent);
                    m_remoteModel.RaiseDeviceChangedEvent += new EventHandler<DeviceChangedArgs>(OnDeviceChangedEvent);
                    m_remoteModel.RaiseMemberChangedEvent += new EventHandler<MemberChangedArgs>(OnMemberChangedEvent);
                }
            }
        }

        /// <summary>
        /// Initialize the synch agent, create the background thread / worker thread that will be used to process
        /// synchronization requests.
        /// </summary>
        public void Initialize()
        {
            m_threadResetEvent = new AutoResetEvent(false);
            m_threadSynchWorker = new Thread(new ThreadStart(BackgroundWorker));
            m_dtNextCheck = DateTime.Now.AddMinutes(DELAY_IN_MINUTES_BEFORE_FIRST_CHECK);
            m_threadSynchWorker.Start();
        }

        /// <summary>
        /// Terminate / close the synchronization agent.
        /// </summary>
        public void Terminate()
        {
            try
            {
                // Tell the worker thread that it should stop working when it wakes up.
                m_bKeepRunning = false;

                // Tell the worker thread to wake up...
                m_threadResetEvent.Reset();

                // Wait for the worker thread to end, wait up to a few seconds.
                bool bTerminated = m_threadSynchWorker.Join(2000);
                if (!bTerminated)
                {
                    System.Diagnostics.Trace.WriteLine("Warning: Sync agent worker thread still running after Terminate().");
                }
            }
            catch (Exception ex)
            {
                System.Diagnostics.Trace.WriteLine("There was an issue with terminating the sync agent's worker thread:" + ex.ToString());
            }
        }

        /// <summary>
        /// Synchronize all relevant data objects between the local model and the remote model.
        /// </summary>
        private void PerformSync()
        {
            // Don't bother if the models are not available (i.e. no internet connectivity).
            if ((!m_localModel.IsAvailable())||(!m_remoteModel.IsAvailable()))
                return;

            // Sync the member records (if any)
            if (m_bSyncMembers)
                PerformMemberSync();

            // Sync the most up to date status for the device(s)
            PerformDeviceSync();

            // Sync (upload) the location history for the device and remove them from the local model
            // once they have been uploaded.
            PerformLocationHistorySync();
        }

        /// <summary>
        /// Synchronize (upload) the location records from the local model to the remote model
        /// and remove them from the local model once they have been successfully uploaded.
        /// </summary>
        private void PerformLocationHistorySync()
        {
            // Retrieve all the location records that were recorded before the time of the next check.
            // Note this logic is a little bit of a hack, since I assume that successfully synched items have
            // been removed from the local model (i.e. a "move" synch),  when I implement a more generic synch 
            // approach that supports both "move" and "copy" type synch, the logic will need to be based on the
            // actual data, i.e. all items "changed" since a timestamp.

            // Convert the current time to UTC since locations from the GPS are collected with respect to GMT / UTC.
            List<WiELocationDataObject> listOfLocationDataObjects = m_localModel.GetLocationHistoryBefore(m_dtNextCheck.ToUniversalTime());
            foreach (WiELocationDataObject locationDataObject in listOfLocationDataObjects)
            {
                // Attempt to save the locationDataObject to the remote data store, give up when we start failing 
                // again a little bit of a hack, I should not assume that failures indicate bad connectivity and have a 
                // separate check for that.
                try
                {
                    // Save to the remote model
                    m_remoteModel.SaveLocation(locationDataObject);

                    // Remove from the local model if the Save succeeded 
                    // (didn't raise an exeception)
                    m_localModel.RemoveLocation(locationDataObject.LocationID);
                }
                catch (Exception ex)
                {
                    // We failed to save the location, so we should give up for now
                    System.Diagnostics.Trace.WriteLine("Unable to save the location during Synch: " + ex.ToString());
                    break;
                }
            }
        }

        /// <summary>
        /// Synchronize (Update) the status for the device on the remote model using the current information
        /// in the local model.
        /// </summary>
        private void PerformDeviceSync()
        {
            List<WiEDeviceDataObject> listOfDevices = m_localModel.GetDevices();
            foreach (WiEDeviceDataObject deviceDataObject in listOfDevices)
            {
                try
                {
                    // Save to the remote model
                    m_remoteModel.SaveDevice(deviceDataObject);
                }
                catch (Exception ex)
                {
                    System.Diagnostics.Trace.WriteLine("Unable to save the device during Synch: " + ex.ToString());
                    break;
                }
            }
        }

        /// <summary>
        /// Copies all member records from the local model to the remote model.
        /// </summary>
        private void PerformMemberSync()
        {
            List<WiEMemberDataObject> listOfMembers = m_localModel.GetMembers();
            foreach (WiEMemberDataObject memberDataObject in listOfMembers)
            {
                try
                {
                    // Save the member to the remote model
                    m_remoteModel.SaveMember(memberDataObject);
                }
                catch (Exception ex)
                {
                    System.Diagnostics.Trace.WriteLine("Unable to save the member during Sync: " + ex.ToString());
                    break;
                }
            }

            // Reset the flag to minimize the # of times we sync member records.
            m_bSyncMembers = false;
        }

        /// <summary>
        /// Entry point for the sync agent background thread.
        /// 
        /// Current implementation only synchs: localModel.Locations --> remoteModel.Locations.
        /// </summary>
        /// <remarks>
        /// 5/19    O.S.M.    Bug Fix. Was not passing in GMT time to the call and all the records from GPS are based on GMT.
        /// </remarks>
        public void BackgroundWorker()
        {
            while (m_bKeepRunning)
            {
                // Wait until we are notified of a change to process or timeout
                m_threadResetEvent.WaitOne(1000,false);
                if (m_bKeepRunning)
                {
                    // Is it time to attempt to synch?
                    if (DateTime.Now >= m_dtNextCheck)
                    {
                        PerformSync();

                        // Let's wait a little bit before we try to synch again
                        m_dtNextCheck = DateTime.Now.AddMinutes(DELAY_IN_MINUTES_BEFORE_NEXT_CHECK);
                    }
                }
            }
        }

        /// <summary>
        /// Public helper method used to wake up the SynchAgent.
        /// </summary>
        /// <remarks>
        /// 7/31 - Bug Fix, was always adding 5 minutes to next check time, so data would never get sent
        /// until there was a pause in incoming data (sort of what we wanted to achieve).
        /// </remarks>
        public void WakeUp()
        {
            DateTime dtMinimumWaitUntil = DateTime.Now.AddMinutes(DELAY_IN_MINUTES_MINIMUM_BETWEEN_CHECK);

            // Force the backgroud process to attempt synch, but make sure there has been at least
            // some decent amount of time since the last time we attempted to sync.
            if (m_dtNextCheck > dtMinimumWaitUntil)
                m_dtNextCheck = dtMinimumWaitUntil;

            try
            {
                m_threadResetEvent.Reset();
            }
            catch (Exception ex)
            {
                // This is a problem, but we can probably keep going.
                System.Diagnostics.Trace.WriteLine("WakeUpWorker() - Raised Exception: " + ex.ToString());
            }
        }

        /// <summary>
        /// Event handler responsible for processing data changed events about the
        /// location history (i.e. new records posted, or records removed).   
        /// 
        /// Depending on implementation these events may not be raised if these records are only saved to remote location.
        /// </summary>
        /// <remarks>
        /// 08-12-08 Bug Fix, was calling WakeUpWorker() instead of WakeUp()
        /// </remarks>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void OnLocationHistoryChangedEvent(object sender, LocationHistoryChangedArgs e)
        {
            if (sender == m_localModel)
            {
                // We've added some new location records to the local data model, we need to make sure these
                // are sent to the remote server so that the device's location can accurately be tracked and
                // rendered.
                WakeUp();
            }
            else
            {
                // Unlikely to occur
            }
        }

        /// <summary>
        /// Event handler responsible for provessing data changed events about a member.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void OnMemberChangedEvent(object sender, MemberChangedArgs e)
        {
            if (sender == m_localModel)
            {
                // We've updated the member on the local client, need to push the updates to the remote model.
                m_bSyncMembers = true;

                // Wake up the sync thread
                WakeUp();
            }
            else
            {
                // The member's information was updated on the server (presumably using web site), need to update 
                // the local version of the member.  We delegate that work to the sync mechanism.
            }
        }

        /// <summary>
        /// Event handler reponsible for processing data changed events about a device.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void OnDeviceChangedEvent(object sender, DeviceChangedArgs e)
        {
            if (sender == m_localModel)
            {
                // We've updated the device information on the local client, need to push the updates to the remote model.
                WakeUp();
            }
            else
            {
                // Unlikely to occur
            }
        }
    }
}
